﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using RanbuOM.Common;

namespace RanbuOM.Serialization
{
    public class RanbuWriter : BinaryWriterEx
    {
        public RanbuWriter(string path, Endianness endianness = Endianness.LittleEndian) 
            : this(File.Create(path), endianness)
        {
        }

        public RanbuWriter(Stream stream, Endianness endianness = Endianness.LittleEndian)
            : base(stream, Encoding.ASCII, endianness)
        {
        }

        public void WriteDataFile(DataFileBase file)
        {
            this.WriteDataType(file);
        }

        private void WriteDataType(DataTypeBase data, string group = null) 
        {
            data.WriteTo(this, group);
        }

        public void WriteDataProperties(DataTypeBase data, string group)
        {
            foreach (var prop in data.GetType().GetProperties())
            {
                var attr = DataPropertyAttribute.GetAttributeOrNull(prop);
                if (attr == null) { continue; }
                if (attr.Group != group) { continue; }
                this.WriteDataProperty(data, prop, attr);
            }
        }

        public void WriteList(IList list, string group = null, IList<Offset32> offsetArray = null)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (offsetArray != null)
                {
                    this.FillOffsetValue(offsetArray[i]);
                }

                var value = list[i];
                if (value is DataTypeBase)
                {
                    this.WriteDataType((DataTypeBase)list[i], group);
                }
                else
                {
                    this.WritePrimitiveValue(list[i]);
                }
            }
        }

        private void WritePrimitiveValue(object value)
        {
            var type = value.GetType();
            if (type.IsEnum) { type = type.GetEnumUnderlyingType(); }

            if (type == typeof(SByte)) { this.WriteI8((sbyte)value); }
            else if (type == typeof(Int16)) { this.WriteI16((short)value); }
            else if (type == typeof(Int32)) { this.WriteI32((int)value); }
            else if (type == typeof(Int64)) { this.WriteI64((long)value); }
            else if (type == typeof(Byte)) { this.WriteU8((byte)value); }
            else if (type == typeof(UInt16)) { this.WriteU16((ushort)value); }
            else if (type == typeof(UInt32)) { this.WriteU32((uint)value); }
            else if (type == typeof(UInt64)) { this.WriteU64((ulong)value); }
            else if (type == typeof(Single)) { this.WriteR4((float)value); }
            else if (type == typeof(Char)) { this.WriteChar((char)value); }
            else
            {
                throw new ArgumentException("value is not primitive type.");
            }
        }

        public Offset32 WriteOffsetPlaceholder(DataTypeBase parent)
        {
            var offset = new Offset32 { Base = parent.Position, Value = 0 };
            this.WriteDataType(offset);
            return offset;
        }

        public void FillOffsetValue(Offset32 offset)
        {
            var current = this.Position;
            offset.Value = (uint)(current - offset.Base);
            this.Seek(offset.Position);
            this.WriteU32(offset.Value);
            this.Seek(current);
        }

        public Offset32[] WriteOffsetPlaceholderArray(DataTypeBase parent, int count)
        {
            var result = new List<Offset32>();
            for (int i = 0; i < count; i++)
            {
                result.Add(this.WriteOffsetPlaceholder(parent));
            }

            return result.ToArray();
        }

        private void WriteDataProperty(DataTypeBase data, PropertyInfo prop, DataPropertyAttribute attr)
        {
            var type = prop.PropertyType;
            var value = prop.GetValue(data, null);

            if (type == typeof(String))
            {
                if (attr.SizeConst == 0)
                {
                    this.WriteCString((string)value);
                }
                else
                {
                    this.WriteCharArrayString((string)value, attr.SizeConst);
                }
            }
            else if (typeof(DataTypeBase).IsAssignableFrom(type))
            {
                this.WriteDataType((DataTypeBase)value);
                if (type == typeof(Offset32))
                {
                    ((Offset32)value).Base = data.Position;
                }
            }
            else if (typeof(IList).IsAssignableFrom(type))
            {
                var list = (IList)value;
                if (attr.SizeConst != list.Count)
                {
                    throw new InvalidOperationException("List count does not match SizeCount.");
                }

                this.WriteList(list);
            }
            else
            {
                this.WritePrimitiveValue(value);
            }
        }
    }
}
